管理后台的侧边栏菜单需要在多种屏幕宽度下提供良好的使用体验:宽屏时展开显示完整菜单,中等宽度时自动折叠为图标模式,移动端则彻底隐藏侧边栏,改为抽屉弹出式菜单。本节使用 VueUse 的 useResizeObserver 实现屏幕宽度监听,结合 Element Plus 的 Drawer 组件,构建一个智能的响应式侧边栏方案。
Flex 布局的侧边栏宽度问题
侧边栏默认使用 flex 布局,当屏幕空间不足时会被其他元素压缩,导致实际宽度与设置的 280px 不符。解决方法是为侧边栏添加 shrink-0(对应 flex-shrink: 0),禁止其被压缩:
<!-- layouts/default/index.vue -->
<template>
<div class="flex h-screen">
<!-- 侧边栏:shrink-0 防止被压缩 -->
<div class="shrink-0" :style="{ width: sidebarWidth }">
<Sidebar />
</div>
<!-- 内容区:使用 flex-grow 自动填充 -->
<div class="flex-grow">
<router-view />
</div>
</div>
</template>
vue
同时需要注意,之前将中间菜单区域从 flex-grow 改为 w-full 后,其他元素(如折叠图标)可能被挤压。如果需要中间区域自适应且其他元素不受影响,应保留 flex-grow 而非使用固定百分比宽度。
屏幕宽度监听与自动折叠
使用 VueUse 提供的 useResizeObserver 监听窗口大小变化,根据宽度阈值自动控制侧边栏的折叠状态:
// composables/useResponsiveCollapse.ts
import { ref, onBeforeMount } from 'vue'
import { useResizeObserver } from '@vueuse/core'
export function useResponsiveCollapse(localSettings: Ref<LocalSettings>) {
const tempWidth = ref(0)
const changeWidthFlag = ref(false)
const isMobile = ref(false)
useResizeObserver(document.body, (entries) => {
const width = entries[0].contentRect.width
// 首次记录宽度
if (tempWidth.value === 0) {
tempWidth.value = width
}
// 判断屏幕缩放方向
const isExpanding = width > tempWidth.value
changeWidthFlag.value = width < 640
// 自动折叠:仅在屏幕缩放方向一致时生效
if (width < 640 && !changeWidthFlag.value) {
localSettings.value.collapse = true
}
if (width > 1200 && changeWidthFlag.value) {
localSettings.value.collapse = false
}
// 移动端判断
isMobile.value = width < 440
tempWidth.value = width
})
// 初始化时检测是否为移动端
onBeforeMount(() => {
const userAgent = navigator.userAgent
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
if (mobileRegex.test(userAgent)) {
isMobile.value = true
localSettings.value.collapse = true
}
})
return { isMobile }
}
typescript
方向感知的折叠逻辑
上述代码中的关键设计是 方向感知:只有当用户的操作方向与折叠/展开的目标一致时,才触发状态变化。例如:
- 用户已经手动折叠了菜单,然后在已折叠状态下继续缩小窗口 —— 不应自动展开
- 用户在宽屏下手动展开菜单,然后继续扩大窗口 —— 不应自动折叠
这通过比较当前宽度与上一次宽度(tempWidth)的大小关系来判断缩放方向,结合 changeWidthFlag 状态量实现。
移动端抽屉菜单
当屏幕宽度小于 440px 时,侧边栏通过设置 min-width: 0 配合动画隐藏,同时使用 Element Plus 的 el-drawer 组件提供弹出式菜单:
<!-- layouts/default/index.vue -->
<template>
<div class="flex h-screen">
<!-- 侧边栏:移动端时宽度为 0 -->
<div
v-show="!isMobile"
class="shrink-0 transition-all duration-300"
:style="{ width: isMobile ? '0px' : sidebarWidth }"
>
<Sidebar :collapse="localSettings.collapse" />
</div>
<!-- 内容区域 -->
<div class="flex-grow overflow-hidden">
<router-view />
</div>
<!-- 移动端抽屉菜单 -->
<el-drawer
v-if="isMobile"
direction="ltr"
:model-value="!localSettings.collapse"
@close="localSettings.collapse = true"
class="w-full"
:style="{ backgroundColor: sidebarBgColor }"
>
<el-menu
:default-active="activeMenu"
:data="menuData"
@select="handleSelect"
/>
</el-drawer>
</div>
</template>
vue
抽屉与折叠按钮的状态同步
移动端抽屉的显示由 localSettings.collapse 控制,但需要取反 —— 因为 collapse 为 true 表示菜单被折叠(图标朝右),此时抽屉应该是隐藏的:
<el-drawer
:model-value="!localSettings.collapse"
@close="localSettings.collapse = true"
/>
vue
注意不能直接使用 v-model 绑定 !localSettings.collapse,因为 v-model 只能接受一个响应式变量而非表达式。因此改用 :model-value + @close 事件的方式手动管理状态。
菜单选择后自动收起
在移动端下,用户选择菜单项后应自动关闭抽屉:
const handleSelect = (index: string) => {
router.push(index)
// 移动端下选择菜单项后自动折叠
if (isMobile.value) {
localSettings.value.collapse = true
}
}
typescript
避免刷新时的闪烁问题
当用户在移动端页面刷新时,由于 isMobile 和 collapse 的计算存在延迟(computed 属性会慢一拍),抽屉会先短暂展开再关闭。解决方法是在 onBeforeMount 中提前将 collapse 设为 true:
onBeforeMount(() => {
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
if (mobileRegex.test(navigator.userAgent)) {
isMobile.value = true
localSettings.value.collapse = true // 防止刷新时闪烁
}
})
typescript
同时,菜单的 onMounted 钩子中自动展开子菜单的逻辑也需要加上条件判断:
onMounted(() => {
// 仅在非折叠状态下展开子菜单
if (!props.collapse) {
openDefaultSubmenu()
}
})
typescript
总结
响应式侧边栏的实现涉及三个层次:
| 屏幕宽度 | 侧边栏行为 | 实现方式 |
|---|---|---|
| > 1200px | 展开显示完整菜单 | 默认状态 |
| 640px ~ 1200px | 自动折叠为图标模式 | useResizeObserver + 方向感知 |
| < 440px | 完全隐藏,使用抽屉弹出 | el-drawer + isMobile 状态 |
核心设计要点是方向感知的折叠逻辑和刷新时的状态预初始化,这两者共同保证了在各种屏幕宽度和操作场景下都能提供流畅、符合预期的用户体验。
↑